
from sys import argv
from utils import *
import midiParser, mido
import datapackGenerator

class MusicGenerator:
    def __init__(self, facing="east"):
        self.COMMANDPLACENORMALCB = "setblock {pos[0]} {pos[1]} {pos[2]} minecraft:command_block[facing={facing}]{{Command:\"{cmd}\", TrackOutput:0b}} replace"
        self.COMMANDPLACECHAINCB = "setblock {pos[0]} {pos[1]} {pos[2]} minecraft:chain_command_block[facing={facing}]{{Command:\"{cmd}\", auto:1b, TrackOutput:0b}} replace"
        self.COMMANDPLACEREPEATCB = "setblock {pos[0]} {pos[1]} {pos[2]} minecraft:repeating_command_block[facing={facing}]{{Command:\"{cmd}\", auto:0b, TrackOutput:0b}} replace"
        self.COMMANDPLACEREPEATER = "setblock {pos[0]} {pos[1]} {pos[2]} minecraft:repeater[facing={facing}, delay={delay}] replace"
        self.COMMANDPLACEWIRE = "setblock {pos[0]} {pos[1]} {pos[2]} minecraft:redstone_wire replace"
        self.COMMANDEXECUTE = "execute at @a run playsound {soundname} master @p ~ ~ ~ {volume:.6f} {tune:.6f}"
        self.COMMANDREMOTEPLACE = "execute positioned {pos[0]} {pos[1]} {pos[2]} run {cmd}"
        self.PITCH = []
        for i in range(25):
            self.PITCH.append(2 ** (-1 + i / 12))
        self.INSTRUMENT = []
        self.INSTRUMENT.append("minecraft:block.note_block.bass")           # F#1-F#3
        self.INSTRUMENT.append("minecraft:block.note_block.didgeridoo")
        self.INSTRUMENT.append("minecraft:block.note_block.guitar")         # F#2-F#4
        self.INSTRUMENT.append("minecraft:block.note_block.iron_xylophone") # F#3-F#5
        self.INSTRUMENT.append("minecraft:block.note_block.bit")
        self.INSTRUMENT.append("minecraft:block.note_block.banjo")
        self.INSTRUMENT.append("minecraft:block.note_block.pling")
        self.INSTRUMENT.append("minecraft:block.note_block.harp")
        self.INSTRUMENT.append("minecraft:block.note_block.flute")          # F#4-F#6
        self.INSTRUMENT.append("minecraft:block.note_block.cow_bell")
        self.INSTRUMENT.append("minecraft:block.note_block.bell")           # F#5-F#7
        self.INSTRUMENT.append("minecraft:block.note_block.chime")
        self.INSTRUMENT.append("minecraft:block.note_block.xylophone")
        self.INSTRUMENT.append("minecraft:block.note_block.snare")
        self.INSTRUMENT.append("minecraft:block.note_block.basedrum")
        self.INSTRUMENT.append("minecraft:block.note_block.hat")

        self.POSITION = [None, None, None]
        
        self.facing = facing
        self.lineCnt = 0
        self.cnt = 0
        self.continual = 0
        self.currentDelta = [0, 0, 0]
        self.extraDelta = [0, 0, 0]
        
    '''
生成命令方块
@param notes A list containing the tune(index) , the instrument(index) and the volume
    '''
    def GenerateCB(self, note, pos, facing):
        ret = [];
        delta = ConvertFacing2Delta(facing)
        cmd = self.COMMANDEXECUTE.format(soundname = self.INSTRUMENT[note[1]], tune = self.PITCH[note[0]], volume=note[2])
        cmd = self.COMMANDPLACENORMALCB.format(pos = ConvertDelta(), facing = facing, cmd = cmd)
        cmd = self.COMMANDREMOTEPLACE.format(pos = pos, cmd = cmd)
        ret.append(cmd)

        cmd = self.COMMANDPLACEWIRE.format(pos = ConvertDelta([0, +1, 0]))
        cmd = self.COMMANDREMOTEPLACE.format(pos = pos, cmd = cmd)
        ret.append(cmd)

        return ret
        
    '''
Be noted that the repeater 'points to' the opposite of the 'facing'
eg: If it has 'facing' set 'south', it points to north 
    '''
    '''
生成指定延时的一连串红石中继器
@param delay Designated delay, in ticks
    '''
    def GenerateDelayArray(self, facing, delay, pos):
        delta = ConvertFacing2Delta(facing)
        num = int(delay / 4)
        residue = delay - num * 4
        ret = []
        for i in range(num):
            cmd = self.COMMANDPLACEREPEATER.format(pos = ConvertDelta([delta[0] * i, delta[1] * i, delta[2] * i]), facing = GetOppositeFacing(facing), delay = 4)
            cmd = self.COMMANDREMOTEPLACE.format(pos = pos, cmd = cmd)
            ret.append(cmd)
        if(residue != 0):
            cmd = self.COMMANDPLACEREPEATER.format(pos = ConvertDelta([delta[0] * num, delta[1] * num, delta[2] * num]), facing = GetOppositeFacing(facing), delay = residue)
            cmd = self.COMMANDREMOTEPLACE.format(pos = pos, cmd = cmd)
            ret.append(cmd)
        return (ret, num if residue == 0 else num + 1)
    '''
生成一个用于中继红石信号的命令方块
    '''
    def GenerateRelay(self, facing, pos):
        delta = ConvertFacing2Delta(facing)
        retVal = []
        cmd = "setblock {pos[0]} {pos[1]} {pos[2]} minecraft:repeater[facing={facing}, powered=true] replace".format(pos = ConvertDelta(delta), facing=GetOppositeFacing(facing))
        cmd = self.COMMANDPLACENORMALCB.format(pos = ConvertDelta(), facing = facing, cmd = cmd)
        cmd = self.COMMANDREMOTEPLACE.format(pos = pos, cmd = cmd)
        retVal.append(cmd)
        cmd = "setblock {pos[0]} {pos[1]} {pos[2]} minecraft:repeater[facing={facing}, powered=false] replace".format(pos = ConvertDelta(delta), facing=GetOppositeFacing(facing))
        cmd = self.COMMANDREMOTEPLACE.format(pos = pos, cmd = cmd)
        retVal.append(cmd)
        cmd = self.COMMANDPLACEWIRE.format(pos = ConvertDelta([0,1,0]))
        cmd = self.COMMANDREMOTEPLACE.format(pos = pos, cmd = cmd)
        retVal.append(cmd)
        return (retVal,2)

    def GenerateRedstoneWire(self, pos):
        cmd = self.COMMANDPLACEWIRE.format(pos = ConvertDelta())
        cmd = self.COMMANDREMOTEPLACE.format(pos = pos, cmd = cmd)
        return cmd

    def resetRelay(self):
        if(20 < self.continual <= 30):
            pass
        elif(10 < self.continual <= 20):
            pass
        elif(0 <= self.continual < 10):
            pass
        else:
            raise ValueError()

    def tryRelay(self):
        if(10 == self.continual):
            self.currentDelta = MoveForward(self.currentDelta, GetOppositeFacing(self.facing), 10)
            self.currentDelta = MoveForward(self.currentDelta, RotateClockwise(self.facing), 1)
        if(20 == self.continual):
            self.currentDelta = MoveForward(self.currentDelta, GetOppositeFacing(self.facing, 10))
            self.currentDelta = MoveForward(self.currentDelta, RotateCounterclockwise(self.facing), 2)
        if(30 == self.continual):
            raise ValueError("To many blocks")

    def tryInsertRelay(self, retVal):
        if(self.continual >= 12 or (self.continual >= 9 and self.cnt == 25)):
            # 连续命令方块过多，插入中继器
            self.continual = 0
            ret = self.GenerateRelay(self.facing, ConvertDelta(self.currentDelta))
            for k in ret[0]:
                retVal.append(k)
            self.currentDelta = MoveForward(self.currentDelta, self.facing, ret[1])

    def tryBreakLine(self, retVal):
        if(self.cnt >= 25):              #每25个命令方块换行
            #print("Breaking line, ", self.cnt)
            self.lineCnt = self.lineCnt + 1
            self.cnt = 0
            self.continual = self.continual + 3
            #向前一格
            ret = self.GenerateRedstoneWire(ConvertDelta(self.currentDelta))
            #print(ret, file = file)
            retVal.append(ret)
            rotation = [0, 0, 0]
            rotationFacing = "east"
            if(self.lineCnt % 2 == 1):
                rotationFacing = RotateClockwise(self.facing)
                rotation = ConvertFacing2Delta(rotationFacing)
            else:
                rotationFacing = RotateCounterclockwise(self.facing)
                rotation = ConvertFacing2Delta(rotationFacing)
            self.currentDelta = MoveForward(self.currentDelta, rotationFacing, 1)
            ret = self.GenerateRedstoneWire(ConvertDelta(self.currentDelta))
            retVal.append(ret)
            self.currentDelta = MoveForward(self.currentDelta, rotationFacing, 1)
            ret = self.GenerateRedstoneWire(ConvertDelta(self.currentDelta))
            retVal.append(ret)
            self.currentDelta = MoveForward(self.currentDelta, rotationFacing, 1)
            ret = self.GenerateRedstoneWire(ConvertDelta(self.currentDelta))
            retVal.append(ret)
            self.currentDelta = MoveForward(self.currentDelta, rotationFacing, 1)
            ret = self.GenerateRedstoneWire(ConvertDelta(self.currentDelta))
            retVal.append(ret)
                
            self.facing = GetOppositeFacing(self.facing)
            
            self.currentDelta = MoveForward(self.currentDelta, self.facing, 1)
            #retVal.append("Pushing to {}".format(self.currentDelta))
    

    def GenerateTimelineByNotes(self, notes):
        retVal = []
        self.currentDelta = [0, 0, 0]
        lastTime = 0

        totalTicks = 0
        lastingNotes = []

        self.cnt = 0
        self.continual = 0
        self.lineCnt = 0
        
        for i in notes:

            try:
                nt = i.toMCNote(+12)      # 升八度，保证音域对应
            except ValueError as e:
                print(e)
                continue
            
            deltaTime = i.start - lastTime
            if(deltaTime < 0):
                raise ValueError("List of notes should be sorted by starting time")
            deltaTick = round(ConvertTick(deltaTime))
            if(deltaTick != 0):     #插入中继器延时
                self.continual = 0
                ret = self.GenerateDelayArray(self.facing, deltaTick, ConvertDelta(self.currentDelta))
                self.currentDelta = MoveForward(self.currentDelta, self.facing, ret[1])
                for k in ret[0]:
                    #print(k, file = file)
                    retVal.append(k)
                
                for j in range(1, deltaTick+1):
                    if(len(lastingNotes) > 0):
                        # 生成一刻延时
                        self.continual = 0
                        ret = self.GenerateDelayArray(self.facing, 1, ConvertDelta(self.currentDelta))
                        self.currentDelta = MoveForward(self.currentDelta, self.facing, ret[1])
                        for k in ret[0]:
                            retVal.append(k)
                        
                        for lasting in lastingNotes:
                            lasting[1] = lasting[1] - 1
                           
                            if(lasting[1] <= 0):
                                del lasting
                                continue
                            ret = self.GenerateCB(lasting[0], ConvertDelta(self.currentDelta), self.facing)
                            retVal.append(ret[0])
                            retVal.append(ret[1])
                            self.currentDelta = MoveForward(self.currentDelta, self.facing, 1)
                            
                            self.continual += 1
                            self.cnt = self.cnt + 1
                            self.tryInsertRelay(retVal)

                            
                            
                            self.tryBreakLine(retVal)
                
            if(deltaTick == 0):
                self.continual = self.continual + 1
                #ret = mg.GenerateRedstoneWire(ConvertDelta(self.currentDelta))
                #self.currentDelta = MoveForward(self.currentDelta, facing, 1)
                #print(ret, file=file)
                #retVal.append(ret)
            lastTime = i.start
            
            
            ret = self.GenerateCB(nt, ConvertDelta(self.currentDelta), self.facing)
            retVal.append(ret[0])
            retVal.append(ret[1])
            self.currentDelta = MoveForward(self.currentDelta, self.facing, 1)
            
            #lastingNotes.append([nt, round(ConvertTick(i.duration))])
            
            self.cnt = self.cnt + 1
            self.tryInsertRelay(retVal)
            self.tryBreakLine(retVal)

            
        return retVal

    def GenerateTimeline(self, midiFile, functionFile = "f.mcfunction"):
        with open(functionFile, mode="w") as file:
            note = midiParser.LoadMidiFile(midiFile)
            ret = self.GenerateTimelineByNotes(note)
            for k in ret:
                print(k, file = file)

if __name__ == "__main__":
    argc = len(argv)
    if(argc != 2):
        print("usage: python *.py *.mid\nPress enter to continue")
    else:
        mg = MusicGenerator()
        #with open("keyboard.mcfunction", mode="w") as file:
        #    lst = mg.GenerateKeyboard([500,21,500])
        #    for k in lst:
        #        print(k, file = file)
        mg.GenerateTimeline(argv[1])

        datapackGenerator.GenerateDatapack()
            
    
